package xyz.lwm.lazycat.vertx;

import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.impl.headers.HeadersMultiMap;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
import xyz.lwm.lazycat.utility.It;
import xyz.lwm.lazycat.utility.PathUtil;
import xyz.lwm.lazycat.utility.StringUtil;
import xyz.lwm.lazycat.web.WebException;
import xyz.lwm.lazycat.web.handle.Context;
import xyz.lwm.lazycat.web.proxy.ProxyClient;
import xyz.lwm.lazycat.web.settings.ProxyHttpSetting;

import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * vertx 代理客户端
 *
 * @author lwm
 */
public class VertxProxyClient implements ProxyClient {

    private final WebClient webClient;

    public VertxProxyClient() {
        WebClientOptions options = new WebClientOptions()
                .setMaxPoolSize(100)                // 每个目标的最大连接数
                .setHttp2MaxPoolSize(100)           // HTTP/2连接池大小
                .setKeepAlive(true)                 // 开启保持连接（必须开启）
                .setPipelining(false)               // 管线化，对于简单转发通常关闭，避免队头阻塞
                .setPipeliningLimit(10)             // 如果开启管线化，设置限制
                .setConnectTimeout(5000)            // 连接超时
                .setIdleTimeout(60)                 // 连接空闲超时（秒）
                .setKeepAliveTimeout(30)            // 保持连接时间（秒）
                .setDecompressionSupported(true);   // 启用解压
        webClient = WebClient.create(Vertx.vertx(), options);
    }

    @Override
    public void proxyHttp(Context ctx, ProxyHttpSetting proxy) {
        // target host
        String targetHost = It.of(proxy.getTargetHost()).getOr("localhost");

        // path
        String newPath = It.of(ctx.getPath()).let(path -> {
            // rewrite 为空时, 则使用 原 path
            if (StringUtil.isBlank(proxy.getRewritePath())) {
                return path;
            }
            // 否则截取替换, 例: prefix path: /api/v1/ -> rewrite path: / ,
            // 可以完成移除前缀 path: /api/v1/users/1 -> /users/1
            String normalizePath = PathUtil.normalizePath(path);
            String prefixPath = PathUtil.normalizePath(proxy.getPrefixPath());
            String subPath = normalizePath.substring(prefixPath.length());
            return PathUtil.of(proxy.getRewritePath()).append(subPath).toString();
        }).get();

        // request http method
        HttpMethod httpMethod = It.of(ctx.req().getMethod()).let(HttpMethod::valueOf).get();

        // request headers
        Map<String, List<String>> headers = ctx.req().getHeaders();
        MultiMap headersMultiMap = new HeadersMultiMap();
        for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
            headersMultiMap.add(entry.getKey(), entry.getValue());
        }

        // request buffer
        Buffer buffer = It.of(ctx.req().getBodyBytes()).let(Buffer::buffer).getOr(Buffer.buffer());

        // 阻塞主线程并等待
        CountDownLatch latch = new CountDownLatch(1);

        webClient.request(httpMethod, proxy.getListenPort(), targetHost, newPath)
                .putHeaders(headersMultiMap).sendBuffer(buffer, res -> {
                    if (res.succeeded()) {
                        ctx.res().setStatus(res.result().statusCode());
                        MultiMap map = res.result().headers();
                        map.forEach(entry ->
                            ctx.res().addHeader(entry.getKey(), entry.getValue()));
                        ctx.res().write(res.result().bodyAsString());
                    } else {
                        ctx.res().setStatus(502);
                        ctx.res().write("BAD FORWARD");
                    }
                    latch.countDown();
                });
        try {
            boolean completed = latch.await(10, TimeUnit.SECONDS);
            if (!completed) {
                throw new WebException("Request timeout");
            }
        } catch (Throwable e) {
            ctx.res().setStatus(502);
            ctx.res().write("BAD FORWARD");
        }
    }

}
